/*
 * Licensed to Nanjing RedOrange Co. ltd (RedOrange) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * RedOrange licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package redora.rdo.impl;

import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import redora.rdo.api.Persistable;
import redora.rdo.api.PersistableSet;
import redora.rdo.exceptions.ObjectNotFoundException;

public class PersistableHashSet<E> extends AbstractSet<E> implements
		PersistableSet<E> {

	private static final long serialVersionUID = 1L;
	protected HashMap<Long, E> map;
	protected int newObjectCount = 0;
	
	/**
	 * List of recently deleted relations with already existing counterparts
	 * (isNew = false, id != null).
	 */
	private Set<E> removedObjects = new HashSet<E>();
	private boolean isRetrieved = false;

	/**
	 * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
	 * default initial capacity (16) and load factor (0.75). <br />
	 * Unchanged from HashMap implementation
	 */
	public PersistableHashSet() {
		map = new HashMap<Long, E>();
	}

	/**
	 * Returns an iterator over the elements in this set. The elements are
	 * returned in no particular order.
	 * 
	 * <br />
	 * Unchanged from HashMap implementation
	 * 
	 * @return an Iterator over the elements in this set
	 * @see ConcurrentModificationException
	 */
	public Iterator<E> iterator() {
		return map.values().iterator();
	}

	/**
	 * Returns the number of elements in this set (its cardinality).
	 * 
	 * <br />
	 * Unchanged from HashMap implementation
	 * 
	 * @see HashMap#size()
	 * @return the number of elements in this set (its cardinality)
	 */
	public int size() {
		return map.size();
	}

	/**
	 * Returns <tt>true</tt> if this set contains no elements. <br />
	 * Unchanged from HashMap implementation
	 * 
	 * @return <tt>true</tt> if this set contains no elements
	 */
	public boolean isEmpty() {
		return map.isEmpty();
	}

	/**
	 * Returns <tt>true</tt> if this set contains the specified element. More
	 * formally, returns <tt>true</tt> if and only if this set contains an
	 * element <tt>e</tt> such that <tt>(o==null ? e==null : o.equals(e))</tt>. <br />
	 * Unchanged from HashMap implementation
	 * 
	 * @param o
	 *            element whose presence in this set is to be tested
	 * @return <tt>true</tt> if this set contains the specified element
	 */
	public boolean contains(Object o) {
		return map.containsValue(o);
	}

	/**
	 * Adds the specified element to this set if it is not already present. More
	 * formally, adds the specified element <tt>e</tt> to this set if this set
	 * contains no element <tt>e2</tt> such that
	 * <tt>(e==null ? e2==null : e.equals(e2))</tt>. If this set already
	 * contains the element, the call leaves the set unchanged and returns
	 * <tt>false</tt>.
	 * 
	 * @param e
	 *            element to be added to this set
	 * @return <tt>true</tt> if this set did not already contain the specified
	 *         element
	 */
	public boolean add(E e) {
		Persistable p = (Persistable) e;
		if (p.getId() != null) {
			return map.put(p.getId(), e) == null;
		}

		return map.put(Long.valueOf(-++newObjectCount), e) == null;
	}

	/**
	 * Convenience method: adds the objects and returns exactly the same object.
	 * Usage: <br />
	 * PersistableParent parent = new PersistableParent();<br />
	 * PersistableChild child = parent.getChildren().insert(new
	 * PersistableChild());<br />
	 * child.setSomething("else");<br />
	 * <br />
	 * service.persist(parent); <br/>
	 * <br/>
	 * Above example creates and persists a new parent and child. So, you need
	 * only line of code to initialize a new child.
	 */
	public E insert(E e) {
		add(e);
		return e;
	}

	/**
	 * Removes the specified element from this set if it is present. More
	 * formally, removes an element <tt>e</tt> such that
	 * <tt>(o==null ? e==null : o.equals(e))</tt>, if this set contains such an
	 * element. Returns <tt>true</tt> if this set contained the element (or
	 * equivalently, if this set changed as a result of the call). (This set
	 * will not contain the element once the call returns.)
	 * 
	 * @param o
	 *            object to be removed from this set, if present
	 * @return <tt>true</tt> if the set contained the specified element
	 */
	@Override
	public boolean remove(Object o) {
		Persistable p = (Persistable) o;
		if (p.getId() != null) {
			return removeById(p.getId());
		}
		return removeNotUsingId(o);
	}

	private boolean removeNotUsingId(Object o) {
		for (long i = -1; i >= -newObjectCount; i--) {
			if (map.containsKey(Long.valueOf(i)) && map.get(i) == o) {
				map.remove(Long.valueOf(i));
				return true;
			}
		}
		return false;
	}

	@Override
	public boolean removeAll(Collection<?> arg0) {
		for (Object o : arg0) {
			Persistable p = (Persistable) o;
			if (!remove(p)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Layer method. Sets everything to the persisted state. Invoke this after
	 * you have persisted all.
	 */
	public void clear() {
		removedObjects.clear();
		newObjectCount = 0;
		isRetrieved = true;
	}

	public E getById(Long id) {
		return map.get(id);
	}

	public boolean removeById(Long id) {
		removedObjects.add(getById(id));
		return map.remove(id) != null;
	}

	/**
	 * Method used by the persistence layer itself. Sets the Id (key) of a new
	 * child after they are persisted in the database.
	 */
	public void setMapKey(E e) throws ObjectNotFoundException {
		Persistable p = (Persistable) e;
		if (getById(p.getId()) == null) {
			if (!removeNotUsingId(p))
				throw new ObjectNotFoundException(
						"Can't remove it from the list because " + e.getClass()
								+ " with id: " + p.getId() + " was not found.",
						null);
			add(e);
		}
	}

	public Set<E> getRemovedObjects() {
		return removedObjects;
	}

	public void setRetrieved(boolean isRetrieved) {
		this.isRetrieved = isRetrieved;
	}

	/**
	 * True when the list is retrieved from the database, or when it is a new
	 * collection. Used for lazy retrieval of children.
	 */
	public boolean isRetrieved() {
		return isRetrieved;
	}

	public boolean isDirty() {
		if (!getRemovedObjects().isEmpty())
			return true;
		if (newObjectCount > 0)
			return true;
		for (E p : map.values())
			if (((Persistable) p).isDirty())
				return true;

		return false;
	}
}
